(function(root, factory) {
  if (typeof define === 'function' && define.amd) {
    define('calendar', ['jquery'], factory);
  } else if (typeof exports === 'object') {
    module.exports = factory(require('jquery'));
  } else {
    factory(root.jQuery);
  }
}(this, function($) {
  var defaults = {
    width: 280,
    height: 280,
    zIndex: 1,
    trigger: null,
    offset: [0, 1],
    customClass: '',
    view: 'date',
    date: new Date(),
    format: 'yyyy/mm/dd',
    startWeek: 0,
    weekArray: ['日', '一', '二', '三', '四', '五', '六'],
    selectedRang: null,
    data: null,
    label: '{d}\n{v}',
    prev: '&lt;',
    next: '&gt;',
    viewChange: $.noop,
    onSelected: function(view, date, value) {},
    onMouseenter: $.noop,
    onClose: $.noop
  }
    , ACTION_NAMESPACE = 'data-calendar-'
    , DISPLAY_VD = '[' + ACTION_NAMESPACE + 'display-date]'
    , DISPLAY_VM = '[' + ACTION_NAMESPACE + 'display-month]'
    , ARROW_DATE = '[' + ACTION_NAMESPACE + 'arrow-date]'
    , ARROW_MONTH = '[' + ACTION_NAMESPACE + 'arrow-month]'
    , ITEM_DAY = ACTION_NAMESPACE + 'day'
    , ITEM_MONTH = ACTION_NAMESPACE + 'month'
    , DISABLED = 'disabled'
    , MARK_DATA = 'markData'
    , VIEW_CLASS = {
    date: 'calendar-d',
    month: 'calendar-m'
  }
    , OLD_DAY_CLASS = 'old'
    , NEW_DAY_CLASS = 'new'
    , TODAY_CLASS = 'now'
    , SELECT_CLASS = 'selected'
    , MARK_DAY_HTML = '<i class="dot"></i>'
    , DATE_DIS_TPL = '{year}/<span class="m">{month}</span>'
    , ITEM_STYLE = 'style="width:{w}px;height:{h}px;line-height:{h}px"'
    , WEEK_ITEM_TPL = '<li ' + ITEM_STYLE + '>{wk}</li>'
    , DAY_ITEM_TPL = '<li ' + ITEM_STYLE + ' class="{class}" {action}>{value}</li>'
    , MONTH_ITEM_TPL = '<li ' + ITEM_STYLE + ' ' + ITEM_MONTH + '>{m}月</li>'
    , TEMPLATE = ['<div class="calendar-inner">', '<div class="calendar-views">', '<div class="view view-date">', '<div class="calendar-hd">', '<a href="javascript:;" data-calendar-display-date class="calendar-display">', '{yyyy}/<span class="m">{mm}</span>', '</a>', '<div class="calendar-arrow">', '<span class="prev" title="上一月" data-calendar-arrow-date>{prev}</span>', '<span class="next" title="下一月" data-calendar-arrow-date>{next}</span>', '</div>', '</div>', '<div class="calendar-ct">', '<ol class="week">{week}</ol>', '<ul class="date-items"></ul>', '</div>', '</div>', '<div class="view view-month">', '<div class="calendar-hd">', '<a href="javascript:;" data-calendar-display-month class="calendar-display">{yyyy}</a>', '<div class="calendar-arrow">', '<span class="prev" title="上一年" data-calendar-arrow-month>{prev}</span>', '<span class="next" title="下一年" data-calendar-arrow-month>{next}</span>', '</div>', '</div>', '<ol class="calendar-ct month-items">{month}</ol>', '</div>', '</div>', '</div>', '<div class="calendar-label"><p>HelloWorld</p><i></i></div>']
    , OS = Object.prototype.toString;
  function isDate(obj) {
    return OS.call(obj) === '[object Date]';
  }
  function isString(obj) {
    return OS.call(obj) === '[object String]';
  }
  function getClass(el) {
    return el.getAttribute('class') || el.getAttribute('className');
  }
  String.prototype.repeat = function(data) {
    return this.replace(/\{\w+\}/g, function(str) {
      var prop = str.replace(/\{|\}/g, '');
      return data[prop] || '';
    });
  }
  String.prototype.toDate = function() {
    var dt = new Date()
      , dot = this.replace(/\d/g, '').charAt(0)
      , arr = this.split(dot);
    dt.setFullYear(arr[0]);
    dt.setMonth(arr[1] - 1);
    dt.setDate(arr[2]);
    return dt;
  }
  Date.prototype.format = function(exp) {
    var y = this.getFullYear()
      , m = this.getMonth() + 1
      , d = this.getDate();
    return exp.replace('yyyy', y).replace('mm', m).replace('dd', d);
  }
  Date.prototype.isSame = function(y, m, d) {
    if (isDate(y)) {
      var dt = y;
      y = dt.getFullYear();
      m = dt.getMonth() + 1;
      d = dt.getDate();
    }
    return this.getFullYear() === y && this.getMonth() + 1 === m && this.getDate() === d;
  }
  Date.prototype.add = function(n) {
    this.setDate(this.getDate() + n);
  }
  Date.prototype.minus = function(n) {
    this.setDate(this.getDate() - n);
  }
  Date.prototype.clearTime = function(n) {
    this.setHours(0);
    this.setSeconds(0);
    this.setMinutes(0);
    this.setMilliseconds(0);
    return this;
  }
  Date.isLeap = function(y) {
    return (y % 100 !== 0 && y % 4 === 0) || (y % 400 === 0);
  }
  Date.getDaysNum = function(y, m) {
    var num = 31;
    switch (m) {
      case 2:
        num = this.isLeap(y) ? 29 : 28;
        break;
      case 4:
      case 6:
      case 9:
      case 11:
        num = 30;
        break;
    }
    return num;
  }
  Date.getSiblingsMonth = function(y, m, n) {
    var d = new Date(y,m - 1);
    d.setMonth(m - 1 + n);
    return {
      y: d.getFullYear(),
      m: d.getMonth() + 1
    };
  }
  Date.getPrevMonth = function(y, m, n) {
    return this.getSiblingsMonth(y, m, 0 - (n || 1));
  }
  Date.getNextMonth = function(y, m, n) {
    return this.getSiblingsMonth(y, m, n || 1);
  }
  Date.tryParse = function(obj) {
    if (!obj) {
      return obj;
    }
    return isDate(obj) ? obj : obj.toDate();
  }
  function Calendar(element, options) {
    this.$element = $(element);
    this.options = $.extend({}, $.fn.calendar.defaults, options);
    this.$element.addClass('calendar ' + this.options.customClass);
    this.width = this.options.width;
    this.height = this.options.height;
    this.date = this.options.date;
    this.selectedRang = this.options.selectedRang;
    this.data = this.options.data;
    this.init();
  }
  Calendar.prototype = {
    constructor: Calendar,
    getDayAction: function(day) {
      var action = ITEM_DAY;
      if (this.selectedRang) {
        var start = Date.tryParse(this.selectedRang[0])
          , end = Date.tryParse(this.selectedRang[1]);
        if ((start && day < start.clearTime()) || (end && day > end.clearTime())) {
          action = DISABLED;
        }
      }
      return action;
    },
    getDayData: function(day) {
      var ret, data = this.data;
      if (data) {
        for (var i = 0, len = data.length; i < len; i++) {
          var item = data[i];
          if (day.isSame(item.date.toDate())) {
            ret = item.value;
          }
        }
      }
      return ret;
    },
    getDayItem: function(y, m, d, f) {
      var dt = this.date, idt = new Date(y,m - 1,d), data = {
        w: this.width / 7,
        h: this.height / 7,
        value: d
      }, markData, $item;
      var selected = dt.isSame(y, m, d) ? SELECT_CLASS : '';
      if (f === 1) {
        data['class'] = OLD_DAY_CLASS;
      } else if (f === 3) {
        data['class'] = NEW_DAY_CLASS;
      } else {
        data['class'] = '';
      }
      if (dt.isSame(y, m, d)) {
        data['class'] += ' ' + TODAY_CLASS;
      }
      data.action = this.getDayAction(idt);
      markData = this.getDayData(idt);
      $item = $(DAY_ITEM_TPL.repeat(data));
      if (markData) {
        $item.data(MARK_DATA, markData);
        $item.html(d + MARK_DAY_HTML);
      }
      return $item;
    },
    getDaysHtml: function(y, m) {
      var year, month, firstWeek, daysNum, prevM, prevDiff, dt = this.date, $days = $('<ol class="days"></ol>');
      if (isDate(y)) {
        year = y.getFullYear();
        month = y.getMonth() + 1;
      } else {
        year = Number(y);
        month = Number(m);
      }
      firstWeek = new Date(year,month - 1,1).getDay() || 7;
      prevDiff = firstWeek - this.options.startWeek;
      daysNum = Date.getDaysNum(year, month);
      prevM = Date.getPrevMonth(year, month);
      prevDaysNum = Date.getDaysNum(year, prevM.m);
      nextM = Date.getNextMonth(year, month);
      var PREV_FLAG = 1
        , CURR_FLAG = 2
        , NEXT_FLAG = 3
        , count = 0;
      for (var p = prevDaysNum - prevDiff + 1; p <= prevDaysNum; p++,
        count++) {
        $days.append(this.getDayItem(prevM.y, prevM.m, p, PREV_FLAG));
      }
      for (var c = 1; c <= daysNum; c++,
        count++) {
        $days.append(this.getDayItem(year, month, c, CURR_FLAG));
      }
      for (var n = 1, nl = 42 - count; n <= nl; n++) {
        $days.append(this.getDayItem(nextM.y, nextM.m, n, NEXT_FLAG));
      }
      return $('<li></li>').width(this.options.width).append($days);
    },
    getWeekHtml: function() {
      var week = []
        , weekArray = this.options.weekArray
        , start = this.options.startWeek
        , len = weekArray.length
        , w = this.width / 7
        , h = this.height / 7;
      for (var i = start; i < len; i++) {
        week.push(WEEK_ITEM_TPL.repeat({
          w: w,
          h: h,
          wk: weekArray[i]
        }));
      }
      for (var j = 0; j < start; j++) {
        week.push(WEEK_ITEM_TPL.repeat({
          w: w,
          h: h,
          wk: weekArray[j]
        }));
      }
      return week.join('');
    },
    getMonthHtml: function() {
      var month = []
        , w = this.width / 4
        , h = this.height / 4
        , i = 1;
      for (; i < 13; i++) {
        month.push(MONTH_ITEM_TPL.repeat({
          w: w,
          h: h,
          m: i
        }));
      }
      return month.join('');
    },
    setMonthAction: function(y) {
      var m = this.date.getMonth() + 1;
      this.$monthItems.children().removeClass(TODAY_CLASS);
      if (y === this.date.getFullYear()) {
        this.$monthItems.children().eq(m - 1).addClass(TODAY_CLASS);
      }
    },
    fillStatic: function() {
      var staticData = {
        prev: this.options.prev,
        next: this.options.next,
        week: this.getWeekHtml(),
        month: this.getMonthHtml()
      };
      this.$element.html(TEMPLATE.join('').repeat(staticData));
    },
    updateDisDate: function(y, m) {
      this.$disDate.html(DATE_DIS_TPL.repeat({
        year: y,
        month: m
      }));
    },
    updateDisMonth: function(y) {
      this.$disMonth.html(y);
    },
    fillDateItems: function(y, m) {
      var ma = [Date.getPrevMonth(y, m), {
        y: y,
        m: m
      }, Date.getNextMonth(y, m)];
      this.$dateItems.html('');
      for (var i = 0; i < 3; i++) {
        var $item = this.getDaysHtml(ma[i].y, ma[i].m);
        this.$dateItems.append($item);
      }
    },
    hide: function(view, date, data) {
      this.$trigger.val(date.format(this.options.format));
      this.options.onClose.call(this, view, date, data);
      this.$element.hide();
    },
    trigger: function() {
      this.$trigger = this.options.trigger instanceof $ ? this.options.trigger : $(this.options.trigger);
      var _this = this
        , $this = _this.$element
        , post = _this.$trigger.offset()
        , offs = _this.options.offset;
      $this.addClass('calendar-modal').css({
        left: (post.left + offs[0]) + 'px',
        top: (post.top + _this.$trigger.outerHeight() + offs[1]) + 'px',
        zIndex: _this.options.zIndex
      });
      _this.$trigger.click(function() {
        $this.show();
      });
      $(document).click(function(e) {
        if (_this.$trigger[0] != e.target && !$.contains($this[0], e.target)) {
          $this.hide();
        }
      });
    },
    render: function() {
      this.$week = this.$element.find('.week');
      this.$dateItems = this.$element.find('.date-items');
      this.$monthItems = this.$element.find('.month-items');
      this.$label = this.$element.find('.calendar-label');
      this.$disDate = this.$element.find(DISPLAY_VD);
      this.$disMonth = this.$element.find(DISPLAY_VM);
      var y = this.date.getFullYear()
        , m = this.date.getMonth() + 1;
      this.updateDisDate(y, m);
      this.updateMonthView(y);
      this.fillDateItems(y, m);
      this.options.trigger && this.trigger();
    },
    setView: function(view) {
      this.$element.removeClass(VIEW_CLASS.date + ' ' + VIEW_CLASS.month).addClass(VIEW_CLASS[view]);
      this.view = view;
    },
    updateDateView: function(y, m, dirc, cb) {
      m = m || this.date.getMonth() + 1;
      var _this = this
        , $dis = this.$dateItems
        , exec = {
        prev: function() {
          var pm = Date.getPrevMonth(y, m)
            , ppm = Date.getPrevMonth(y, m, 2)
            , $prevItem = _this.getDaysHtml(ppm.y, ppm.m);
          m = pm.m;
          y = pm.y;
          $dis.animate({
            marginLeft: 0
          }, 300, 'swing', function() {
            $dis.children(':last').remove();
            $dis.prepend($prevItem).css('margin-left', '-100%');
            $.isFunction(cb) && cb.call(_this);
          });
        },
        next: function() {
          var nm = Date.getNextMonth(y, m)
            , nnm = Date.getNextMonth(y, m, 2)
            , $nextItem = _this.getDaysHtml(nnm.y, nnm.m);
          m = nm.m;
          y = nm.y;
          $dis.animate({
            marginLeft: '-200%'
          }, 300, 'swing', function() {
            $dis.children(':first').remove();
            $dis.append($nextItem).css('margin-left', '-100%');
            $.isFunction(cb) && cb.call(_this);
          });
        }
      };
      if (dirc) {
        exec[dirc]();
      } else {
        this.fillDateItems(y, m);
      }
      this.updateDisDate(y, m);
      this.setView('date');
      return {
        y: y,
        m: m
      };
    },
    updateMonthView: function(y) {
      this.updateDisMonth(y);
      this.setMonthAction(y);
      this.setView('month');
    },
    getDisDateValue: function() {
      var arr = this.$disDate.html().split('/')
        , y = Number(arr[0])
        , m = Number(arr[1].match(/\d{1,2}/)[0]);
      return [y, m];
    },
    selectedDay: function(d, type) {
      var arr = this.getDisDateValue()
        , y = arr[0]
        , m = arr[1]
        , toggleClass = function() {
        this.$dateItems.children(':eq(1)').find('[' + ITEM_DAY + ']:not(.' + NEW_DAY_CLASS + ', .' + OLD_DAY_CLASS + ')').removeClass(SELECT_CLASS).filter(function(index) {
          return parseInt(this.innerHTML) === d;
        }).addClass(SELECT_CLASS);
      };
      if (type) {
        var ret = this.updateDateView(y, m, {
          'old': 'prev',
          'new': 'next'
        }[type], toggleClass);
        y = ret.y;
        m = ret.m;
        this.options.viewChange('date', y, m);
      } else {
        toggleClass.call(this);
      }
      return new Date(y,m - 1,d);
    },
    showLabel: function(event, view, date, data) {
      var $lbl = this.$label;
      $lbl.find('p').html(this.options.label.repeat({
        m: view,
        d: date.format(this.options.format),
        v: data
      }).replace(/\n/g, '<br>'));
      var w = $lbl.outerWidth()
        , h = $lbl.outerHeight();
      $lbl.css({
        left: (event.pageX - w / 2) + 'px',
        top: (event.pageY - h - 20) + 'px'
      }).show();
    },
    hasLabel: function() {
      if (this.options.label) {
        $('body').append(this.$label);
        return true;
      }
      return false;
    },
    event: function() {
      var _this = this
        , vc = _this.options.viewChange;
      _this.$element.on('click', DISPLAY_VD, function() {
        var arr = _this.getDisDateValue();
        _this.updateMonthView(arr[0], arr[1]);
        vc('month', arr[0], arr[1]);
      }).on('click', DISPLAY_VM, function() {
        var y = this.innerHTML;
        _this.updateDateView(y);
        vc('date', y);
      });
      _this.$element.on('click', ARROW_DATE, function() {
        var arr = _this.getDisDateValue()
          , type = getClass(this)
          , y = arr[0]
          , m = arr[1];
        var d = _this.updateDateView(y, m, type, function() {
          vc('date', d.y, d.m);
        });
      }).on('click', ARROW_MONTH, function() {
        var y = Number(_this.$disMonth.html())
          , type = getClass(this);
        y = type === 'prev' ? y - 1 : y + 1;
        _this.updateMonthView(y);
        vc('month', y);
      });
      _this.$element.on('click', '[' + ITEM_DAY + ']', function() {
        var d = parseInt(this.innerHTML)
          , cls = getClass(this)
          , type = /new|old/.test(cls) ? cls.match(/new|old/)[0] : '';
        var day = _this.selectedDay(d, type);
        _this.options.onSelected.call(this, 'date', day, $(this).data(MARK_DATA));
        _this.$trigger && _this.hide('date', day, $(this).data(MARK_DATA));
      }).on('click', '[' + ITEM_MONTH + ']', function() {
        var y = Number(_this.$disMonth.html())
          , m = parseInt(this.innerHTML);
        _this.updateDateView(y, m);
        vc('date', y, m);
        _this.options.onSelected.call(this, 'month', new Date(y,m - 1));
      });
      _this.$element.on('mouseenter', '[' + ITEM_DAY + ']', function(e) {
        var arr = _this.getDisDateValue()
          , day = new Date(arr[0],arr[1] - 1,parseInt(this.innerHTML));
        if (_this.hasLabel && $(this).data(MARK_DATA)) {
          $('body').append(_this.$label);
          _this.showLabel(e, 'date', day, $(this).data(MARK_DATA));
        }
        _this.options.onMouseenter.call(this, 'date', day, $(this).data(MARK_DATA));
      }).on('mouseleave', '[' + ITEM_DAY + ']', function() {
        _this.$label.hide();
      });
    },
    resize: function() {
      var w = this.width
        , h = this.height
        , hdH = this.$element.find('.calendar-hd').outerHeight();
      this.$element.width(w).height(h + hdH).find('.calendar-inner, .view').css('width', w + 'px');
      this.$element.find('.calendar-ct').width(w).height(h);
    },
    init: function() {
      this.fillStatic();
      this.resize();
      this.render();
      this.view = this.options.view;
      this.setView(this.view);
      this.event();
    },
    setData: function(data) {
      this.data = data;
      if (this.view === 'date') {
        var d = this.getDisDateValue();
        this.fillDateItems(d[0], d[1]);
      } else if (this.view === 'month') {
        this.updateMonthView(this.$disMonth.html());
      }
    },
    methods: function(name, args) {
      if (OS.call(this[name]) === '[object Function]') {
        return this[name].apply(this, args);
      }
    }
  };
  $.fn.calendar = function(options) {
    var calendar = this.data('calendar'), fn, args = [].slice.call(arguments);
    if (!calendar) {
      return this.each(function() {
        return $(this).data('calendar', new Calendar(this,options));
      });
    }
    if (isString(options)) {
      fn = options;
      args.shift();
      return calendar.methods(fn, args);
    }
    return this;
  }
  $.fn.calendar.defaults = defaults;
}));
